/**
 * Copyright (C) 2007 Transmute Technologies. All rights reserved.
 * ===============================================================
 * Component Desc : 
 * Major :
 * Minor :
 * @author Naveen Kumar
 * Mail : unaveenkumar@gmail.com
 * Created on : Jan 14, 2007
 *  
 * The program is provided "as is" without any warranty express or
 * implied, including the warranty of non-infringement and the implied
 * warranties of merchantibility and fitness for a particular purpose.
 *
 * Modified by : Jan 14, 2007
 * Revision number : 0.5
 *
 *	Revision History
 *	----------------
 *
 *	0.5
 *		Added the minConnectionCount { the number of min connections 
 *				to exists }
 *		
 */
package org.transmute.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

/**
 * Database connection are implemented here
 * 
 * @author Naveen Kumar
 * 
 */
public class ConnectionPooling implements Runnable {
	public static final int ALREADY_STARTED = 51;

	private static ConnectionPooling connectionPooling = new ConnectionPooling();

	private static String listExcededMessage = "Total number of allowed Databases has been exhausted.";

	private static String poolExcededMessage = "Number of Active Connections to the Database has been exhausted.";

	public static final int REFRESH = 3;

	public static final int START = 1;

	public static final int STARTED = 50;

	public static final int STOP = 2;

	public static final int STOPPED = 54;

	public static final int UNLIMITED = 0;

	/**
	 * returns ConnectionManager Object
	 * 
	 * @return ConnectionManager
	 */
	public static ConnectionPooling getConnectionPool() {
		return connectionPooling;
	}

	// Total number of connections allowed of a particular type
	private int connectionsCount = 25;

	// To set whether the cleanup is to be performed or not
	private boolean forceCleanUp = false;

	private boolean forceClose = false;

	private boolean isDone = false;

	// Total number of different connections allowed
	private int maxDiffConnections = 5;

	// Minimum number of connections in pool
	private int minConnectionCount = 0;

	private HashMap pool = null;

	// maintain as a singleton
	private Thread pooler = null;

	private boolean poolingFlag = false;

	// Monitor at regular interval specifed below(3mins)
	private long poolMonitorTime = 2 * 60 * 1000;

	// This will queue the connection request if in case active connection has
	// reached connectionCount
	private boolean queueConnections = true;

	private String status = "Yet To Start";

	/**
	 * <p/> No argument constructor for initializing ConnectionPooling object.
	 * </p>
	 */
	private ConnectionPooling() {
		if (pool == null) {
			pool = new HashMap();
			pooler = new Thread(this, "ConnectionPoolService");
			// logger.debug("Connection pool initialized.. ");
		}
	}

	/**
	 * <p/> Clears INACTIVE connections by checking the session timed out of
	 * individual connection
	 * </p>
	 */
	private synchronized void cleanConnectionPool() {
		// The pool doesnt shows the total number of database connections.
		// The pool is the container for the one particular type of database
		// connections and thus
		// it shows the number of different connections and not the number of
		// connections.

		// logger.debug("Total number of pools available is => " + pool.size());
		if (pool == null) {
			return;
		}

		Iterator key = pool.keySet().iterator();
		ArrayList connList = null;

		while (key.hasNext()) {
			String detailsKey = (String) key.next();
			connList = (ArrayList) pool.get(detailsKey);

			if (minConnectionCount >= connList.size()) {
				// The arrayList already has less number of connections.
				// So
				// continue with the next arraylist
				continue;
			}
			for (int i = connList.size() - 1; i >= minConnectionCount; i--) {
				ConnectionWrapper wrapper = (ConnectionWrapper) connList.get(i);
				// Even if the connection is closed explicitly
				// getConnection will return null
				if (forceClose && wrapper.isTimeOut()) {
					System.out.println("Force timeout has killed the proceess" + wrapper.hashCode());
					wrapper.closeConnection();
					connList.remove(i);
					if (connList.size() <= 0) {
						pool.remove(detailsKey);
					}
					continue;
				}
				if (wrapper.getConnection() == null) {
					connList.remove(i);
					// pool.remove(detailsKey);
					if (connList.size() <= 0) {
						pool.remove(detailsKey);
					}
				} // check for session time and close it accordingly
				else if (!wrapper.isAssigned() && wrapper.isTimeOut()) {
					// logger.debug("Cleaned the connection " +
					// detailsKey);
					wrapper.closeConnection();
					// You are suppose to remove that particular
					// connection from
					// the arrayList
					// and not all the connections of the same key from
					// the
					// hashMAp
					connList.remove(i);
					// pool.remove(detailsKey);
					if (connList.size() <= 0) {
						pool.remove(detailsKey);
					}
				}
			}

		}

	}

	/**
	 * <p/> Method to create a conneciton object.
	 * </p>
	 * 
	 * @param ConnectionDetails -
	 *            Object containing the connection details.
	 * @return ConnectionWrapper - Wrapper object that wraps the connection
	 *         object returned.
	 * @throws SQLException
	 * @see ConnectionWrapper
	 * @see ConnectionDetails
	 */
	private ConnectionWrapper createConnection(ConnectionDetails connDetail)
			throws SQLException {
		ConnectionWrapper connWrap = null;
		String driverName = connDetail.getDriverUrl();
		String connectionString = connDetail.getDbUrl();
		String userName = connDetail.getUserId();
		String password = connDetail.getPassword();

		try {
			if (connDetail != null) {
				Class.forName(driverName);
				Connection conn = null;

				if (((userName == null) || (userName.length() == 0))
						&& ((password == null) || (password.length() == 0))) {
					conn = DriverManager.getConnection(connectionString);
				} else {
					conn = DriverManager.getConnection(connectionString,
							userName, password);
				}

				if (conn != null) {
					connWrap = new ConnectionWrapper(conn, connDetail, true);
				} else {
					throw new SQLException("Unable to create connection");
				}
			}
		} catch (SQLException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new SQLException("Connection creation failure "
					+ ex.getMessage());
		}

		return connWrap;
	}

	// /**
	// * <p/> Method to close connection from the connection pool.
	// * </p>
	// *
	// * @return void.
	// */
	// private void closeConnections(ArrayList connWrapList) {
	// ConnectionWrapper connectionWrapper = null;
	// synchronized (connWrapList) {
	// if ((connWrapList != null) && (!connWrapList.isEmpty())) {
	// int removeIndex = 0;
	//
	// for (int i = 0, len = connWrapList.size(); i < len; i++) {
	// connectionWrapper = (ConnectionWrapper) connWrapList.get(i);
	//
	// if ((connectionWrapper != null)
	// && !connectionWrapper.isAssigned()) {
	// connectionWrapper.closeConnection();
	// connWrapList.remove(removeIndex);
	// } else {
	// removeIndex++;
	// }
	// }
	// }
	// }
	// }

	/**
	 * Closes all connections
	 */
	public void finalize() throws Throwable {
		removeAllConnections();
	}

	/**
	 * returns a connection wrapper which wraps connection object.
	 * 
	 * @param connDetail -
	 *            Connection Details.
	 * @throws DAOException
	 * @see ConnectionWrapper
	 */
	public synchronized ConnectionWrapper getConnection(
			ConnectionDetails connDetail) throws SQLException {
		ConnectionWrapper connectionWrapper = null;
		String connectionKey = connDetail.formKey();
		// If the service not started ... start the pooler service
		if (!poolingFlag) {
			// throw new SQLException("Connection Pool Service Not Yet
			// Started");
			this.startPool();
		}
		// If pool not exist initialize the pool
		if (pool == null) {
			pool = new HashMap();
		}
		ArrayList connectionList = null;
		if (pool.get(connectionKey) == null) {
			if (maxDiffConnections != UNLIMITED
					&& pool.size() >= maxDiffConnections) {
				throw new SQLException(listExcededMessage);
			}
			connectionList = new ArrayList();
			pool.put(connectionKey, connectionList);
		} else {
			connectionList = (ArrayList) pool.get(connectionKey);
		}
		try {
			// if pool contains connections pick from the pool
			if (!connectionList.isEmpty()) {
				connectionWrapper = getValidConnection(connDetail,
						connectionList);
				// Force the clean up
				// NO need to do the clean up here and waste the users time
				// if (forceCleanUp) {
				// cleanConnectionPool();
				// }
				if (connectionWrapper != null) {
					connectionWrapper.assignConnection();
					return connectionWrapper;
				}
			}
			// Checks if connection has exceeded. This method can make
			// thread wait if queueConnection is set
			if (connectionsCount != UNLIMITED) {
				if (hasConnectionExceeded(connectionList)) {
					if (queueConnections) {
						while (hasConnectionExceeded(connectionList)) {
							this.wait(50);
							connectionWrapper = getValidConnection(connDetail,
									connectionList);
							if (connectionWrapper != null) {
								connectionWrapper.assignConnection();
								return connectionWrapper;
							}
						}
						if (!hasConnectionExceeded(connectionList)) {
							connectionWrapper = createConnection(connDetail);
							// connectionWrapper.setHashCode(connectionList.size()+((int)(Math.random()*100000))+"");
							connectionList.add(connectionWrapper);
							return connectionWrapper;
						}
					} else {
						// The connection count is unlimited and
						throw new SQLException(poolExcededMessage);
					}
				} else {
					connectionWrapper = createConnection(connDetail);
					// connectionWrapper.setHashCode(connectionList.size()+((int)(Math.random()*100000))+"");
					connectionList.add(connectionWrapper);
					return connectionWrapper;
				}
			} else {
				connectionWrapper = createConnection(connDetail);
				// connectionWrapper.setHashCode(connectionList.size()+((int)(Math.random()*100000))+"");
				connectionList.add(connectionWrapper);
			}
		} catch (SQLException sql) {
			throw sql;
		} catch (Exception exp) {
			// logger.error("Error Raised in getConnectionWrapper: " +
			// exp.getMessage());
			throw new SQLException(exp.getMessage());
		}

		if (connectionWrapper == null) {
			// logger.debug("Not able to retrieve (pool) or create a
			// connection");
			throw new SQLException("General Exception from Connection Wrapper");
		}

		return connectionWrapper;
	}

	/**
	 * Returns the minimum number of database connection
	 * 
	 * @return minConn
	 */
	private int getMinConnectionCount() {
		return minConnectionCount;
	}

	/**
	 * Gets a valid free connection from the connectionList given the
	 * connectionDetails
	 * 
	 * @param connectionDetails
	 * @param connWrapList
	 * @return
	 * @throws SQLException
	 */
	private synchronized ConnectionWrapper getValidConnection(
			ConnectionDetails connectionDetails, ArrayList connWrapList)
			throws SQLException {
		ConnectionWrapper connectionWrapper = null;
		try {
			if ((connWrapList != null) && (!connWrapList.isEmpty())) {
				for (int i = 0, len = connWrapList.size(); i < len; i++) {
					connectionWrapper = (ConnectionWrapper) connWrapList.get(i);
					if (!connectionWrapper.isTimeOut()) {
						if (!connectionWrapper.isAssigned()
								&& connectionWrapper.isValid()) {
							return connectionWrapper;
						}
					} else {
						// Connection is not required any more go for clean
						// up
						// by setting flag
						forceCleanUp = true;
					}
				}
			}
		} catch (Exception exp) {
		}
		return null;
	}

	/**
	 * @return Returns true if connection size has exceeded
	 */
	private synchronized boolean hasConnectionExceeded(ArrayList connectionList) {
		if (connectionList.size() >= connectionsCount) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Method to show whether the pooling is done and the program is to be
	 * stopped
	 * 
	 * @return boolean
	 */
	public boolean isDone() {
		return isDone;
	}

	// /**
	// * @return Returns true if connection size has exceeded
	// */
	// private boolean hasConnectionExceeded(ArrayList connectionList)
	// throws Exception {
	// if (connectionList.size() >= connectionsCount) {
	// if (queueConnections) {
	// Thread.sleep(10);
	// } else {
	// throw new SQLException(poolExcededMessage);
	// }
	// return true;
	// } else {
	// return false;
	// }
	// }

	/**
	 * Method to get back whether the connection can be queued or not
	 * 
	 * @return boolean
	 */
	public boolean isQueueEnabled() {
		return queueConnections;
	}

	/**
	 * Method to check if the given connection exists in the connection pool.
	 * 
	 * @param conn
	 *            Connection
	 * @return connIsPooled true / false
	 */
	public boolean pooledConnection(Connection conn) {
		if (conn instanceof ConnectionWrapper) {
			/**
			 * @TODO : To check if the connection exists in connWrapList
			 */
			return true;
		}
		return false;
	}

	/**
	 * Method to release the connection back to the pool
	 * 
	 * @param conn
	 *            Connection created using the connection pool
	 */
	public synchronized void releaseConnection(Connection conn) {
		if (conn instanceof ConnectionWrapper) {
			ConnectionWrapper conn2 = (ConnectionWrapper) conn;
			// No need to sync here becoz at connectionWrapper level its done
			conn2.releaseConnection();
		} else {
			System.out.println("Do not try to release a non pooled connection");
		}
	}

	/**
	 * <p/> Method to remove all the connections from the connection pool.
	 * </p>
	 * 
	 * @return void.
	 */
	private synchronized void removeAllConnections() {
		if (pool == null) {
			return;
		}
		Iterator key = pool.keySet().iterator();
		ArrayList connList = null;
		while (key.hasNext()) {
			String detailsKey = (String) key.next();
			connList = (ArrayList) pool.get(detailsKey);
			for (int i = connList.size() - 1; i >= 0; i--) {
				ConnectionWrapper wrapper = (ConnectionWrapper) connList.get(i);
				if (wrapper == null)
					continue;
				wrapper.closeConnection();
				// You are suppose to remove that particular connection
				// from
				// the arrayList
				// and not all the connections of the same key from the
				// hashMAp
				connList.remove(i);
			}
			connList = null;
			pool.remove(detailsKey);
		}
		pool = null;
	}

	// /**
	// *
	// * Method to print the available connections in the pool
	// */
	// private void printConnections() {
	// if ((pool != null) && (!pool.isEmpty())) {
	// ArrayList totalConnectionList = new ArrayList(pool.values());
	// for (int i = 0, len = totalConnectionList.size(); i < len; i++) {
	// printConnections((ArrayList) totalConnectionList.get(i));
	// }
	// }
	// }
	//
	// /**
	// *
	// * @param connWrapList
	// */
	// private void printConnections(ArrayList connWrapList) {
	// ConnectionWrapper connWrap = null;
	// synchronized (connWrapList) {
	// if ((connWrapList != null) && (!connWrapList.isEmpty())) {
	// for (int i = 0, len = connWrapList.size(); i < len; i++) {
	// connWrap = (ConnectionWrapper) connWrapList.get(i);
	// if (connWrap != null) {
	// System.out.println("This is the connection "
	// + connWrap.hashCode());
	// }
	// }
	// }
	// }
	// }

	/**
	 * <p/> Method to close the list of connections from the connection pool of
	 * the connection detail specified.
	 * </p>
	 * 
	 * @return void.
	 */
	private synchronized void removeAllConnections(ArrayList connWrapList) {
		ConnectionWrapper connWrap = null;
		if ((connWrapList != null) && (!connWrapList.isEmpty())) {
			for (int i = 0, len = connWrapList.size(); i < len; i++) {
				connWrap = (ConnectionWrapper) connWrapList.get(i);
				if (connWrap != null) {
					connWrap.closeConnection();
				}
			}
			connWrapList = null;
		}
	}

	/**
	 * <p/> Method to manage the connection pool.
	 * </p>
	 * 
	 * @return void.
	 */
	public void run() {
		while (!isDone()) {
			status = "Connection Pool Started";
			try {
				cleanConnectionPool();
				synchronized (this) {
					this.wait(poolMonitorTime);
				}
			} catch (InterruptedException ie) {
				// logger.error("Error in pool thread ,", ie);
				if (isDone())
					return;
			}
		}
	}

	/**
	 * Marks the Pooler as either a daemon thread or a user thread.
	 */
	public void setDeamon(boolean on) {
		try {
			synchronized (this) {
				pooler.setDaemon(on);
			}
		} catch (Exception excep) {
			System.out
					.println("Do not try to change the thread when its active");
		}
	}

	/**
	 * Set the value to true to force close the connection after timeout
	 */
	public void setForceClose(boolean forceClose) {
		this.forceClose = forceClose;
	}

	/**
	 * Method to set the maximum number of database connections of a particular
	 * database
	 * 
	 * @param count
	 */
	public void setMaxConnection(int count) {
		if (count >= 1)
			connectionsCount = count;
	}

	/**
	 * Method to set the maximum number of different database allowed
	 * 
	 * @param count
	 */
	public void setMaxDiffConnections(int count) {
		if (count >= 1)
			maxDiffConnections = count;
	}

	/**
	 * MEthod to set the unlimited database can be connected
	 * 
	 */
	public void setMaxDiffConnectionsUnlimited() {
		maxDiffConnections = UNLIMITED;
	}

	public void setMinConnectionCount(int minCount) {
		if (minCount >= 0)
			this.minConnectionCount = minCount;
	}

	/**
	 * Sets the time period for monitoring the pool in milli sec
	 */
	public void setMonitorTime(int monitorTime) {
		poolMonitorTime = monitorTime;
	}

	/**
	 * Sets whether the extra connections request to be queued or not. If
	 * queued, it will have the time interval of 10 milli sec
	 * 
	 * @param enable
	 *            true / false
	 */
	public void setQueue(boolean enable) {
		queueConnections = enable;
	}

	/**
	 * Method to make the poolsize unlimited
	 */
	public void setUnlimited() {
		connectionsCount = UNLIMITED;
	}

	/**
	 * Starts the connection Pooling
	 */
	public void startConnectionPool() {
		startPool();
	}

	/**
	 * <p/> Starts the connection pool.
	 * </p>
	 * 
	 * @return int - Status code specifying whether the connection pool is
	 *         started or not.
	 */
	private int startPool() {
		synchronized (this) {
			if (poolingFlag) {
				return ALREADY_STARTED;
			}
			poolingFlag = true;
			pooler.start();
		}
		return STARTED;
	}

	/**
	 * Stops the connection Pooling
	 */
	public void stopConnectionPool() {
		stopPool();
	}

	/**
	 * <p/> Stops the connection pool.
	 * </p>
	 * 
	 * @return int - Status code specifying whether the connection pool is
	 *         stopped or not.
	 */
	private synchronized void stopPool() {
		poolingFlag = false;
		// printConnections();
		removeAllConnections();
		// printConnections();
		pool = null;
		isDone = true;
		pooler.interrupt();
	}
}
